/* JavaLog - package tracker 
 * 
 * Copyright (C) 2009  Allyson Lima, Diego Pedro, Victor Freire
 * 
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software 
 * Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this 
 * program.  If not, see <http://www.gnu.org/licenses/>.
 */

package br.edu.ufcg.ccc.javalog.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import br.edu.ufcg.ccc.javalog.allshortestpath.FloydWarshall;
import br.edu.ufcg.ccc.javalog.event.FranchiseEvent;
import br.edu.ufcg.ccc.javalog.exceptions.FranchiseException;
import br.edu.ufcg.ccc.javalog.exceptions.PackageException;
import br.edu.ufcg.ccc.javalog.listener.FranchiseListener;

/**
 * Represents a generic store.
 * 
 * @author Allyson Lima, Diego Pedro, Victor Freire
 * @version 08/11/09
 */
public class Franchise implements Comparable<Franchise> {
	private static Map<String, Package> allPackages = new HashMap<String, Package>(); // PackageID
	// -Package

	private Set<Package> packagesInTransit = new HashSet<Package>(); // packages
	// here just passing by, waiting to be forwared to the next store
	private Set<Package> packagesToBeDelivered = new HashSet<Package>(); // packages
	// here that are waiting to be delivered to their owners

	private List<FranchiseListener> franchiseListeners = new ArrayList<FranchiseListener>();

	private String name;
	private Address address;
	private Franchise airport;
	private int packagesHandled;
	private int sales;
	private List<Franchise> nearestFranchises = new ArrayList<Franchise>();

	private int vertex;
	private static int INDEX_VERTEX = 0;

	private static FloydWarshall distances;

	private Location franchiseLocation;

	/**
	 * Constructs a new Franchise.
	 * 
	 * @param name
	 *            the name of the franchise
	 * @param address
	 *            the address of the franchise
	 * @param airport
	 *            does the franchise has an airport?
	 * @throws IllegalArgumentException
	 *             parameters that failed default validation by Validator
	 */
	public Franchise(String name, Address address, boolean airport)
			throws FranchiseException {
		Validator
				.validatePhrase(name, new FranchiseException("Nome inválido."));
		Validator.validateObject(address, new FranchiseException(
				"Endereço inválido."));
		this.name = name;
		this.address = address;
		// TODO write getNearestAirport()
		// if(airport) airport = getNearestAirport();

		// TODO calculate nearestFranchises

		setVertex(INDEX_VERTEX++);
	}

	/**
	 * Constructs a new Franchise.
	 * 
	 * @param name
	 *            the name of the franchise
	 * @param address
	 *            the address of the franchise
	 * @param airport
	 *            does the franchise has an airport?
	 * @param franchiseLocation
	 *            Location this franchise
	 * @throws IllegalArgumentException
	 *             parameters that failed default validation by Validator
	 */
	public Franchise(String name, Address address, boolean airport,
			Location franchiseLocation) throws FranchiseException {
		Validator
				.validatePhrase(name, new FranchiseException("Nome inválido."));
		Validator.validateObject(address, new FranchiseException(
				"Endereço inválido."));
		this.name = name;
		this.address = address;
		// TODO write getNearestAirport()
		// if(airport) airport = getNearestAirport();

		// TODO calculate nearestFranchises

		setVertex(INDEX_VERTEX++);

		this.franchiseLocation = franchiseLocation;
	}

	/**
	 * Returns the location of the franchise
	 * 
	 * @return the location of the franchise
	 */
	public Location getLocation() {
		return franchiseLocation;
	}

	/**
	 * Modifies the location this franchise
	 * 
	 * @param franchiseLocation
	 *            Location this franchise
	 */
	public void setLocation(Location franchiseLocation) {
		this.franchiseLocation = franchiseLocation;
	}

	/**
	 * Return the vertex of the graph
	 * 
	 * @return the vertex of the graph
	 */
	public int getVertex() {
		return vertex;
	}

	/**
	 * Modifies the vertex of the graph
	 * 
	 * @param vertex
	 *            Vertex of the graph
	 */
	public void setVertex(int vertex) {
		this.vertex = vertex;
	}

	/**
	 * Returns next franchise in shortest path
	 * 
	 * @param destinationFranchise
	 *            Destination Franchise
	 * @return next franchise in shortest path
	 */
	public Franchise getNextFranchiseInShortestPath(
			Franchise destinationFranchise) {
		return distances.getNextFranchise(this, destinationFranchise);
	}

	/**
	 * Returns the map relating package ids and packages.
	 */
	public static Map<String, Package> getPackages() {
		return allPackages;
	}

	/**
	 * Notifies this Franchise of the arrival of a new package. Retrieves the
	 * package by its ID and calls receivePackage(Package pkg).
	 * 
	 * @param packageID
	 *            the id of the package received
	 * @throws PackageException
	 *             if the package with the ID specified cannot be found
	 */
	public void receivePackage(String packageID) throws PackageException {
		Validator.validatePackageID(packageID, new PackageException(
				"Invalid ID package"));
		Package pkg = allPackages.get(packageID);
		Validator.validateObject(pkg, new PackageException(
				"The package requested was not found!"));
		receivePackage(pkg);
	}

	/**
	 * Notifies this Franchise of the arrival of a new package.
	 * 
	 * @param pkg
	 *            the package received
	 * @throws PackageException
	 *             when a null package is passed
	 */
	public void receivePackage(Package pkg) throws PackageException {
		Validator.validateObject(pkg, new PackageException(
				"Package validation failed"));

		// Update packages handled counter.
		++packagesHandled;

		// TODO Update package tracking report
	}

	// TODO finish createPackage()
	public void createPackage(Package p) {
		Validator.validateObject(p, new IllegalArgumentException(
				"null package received"));
		allPackages.put(p.getID(), p);
		packagesInTransit.add(p);
	}

	/**
	 * Moves all packages in transit
	 * 
	 * @throws PackageException
	 *             Illegal entry for package
	 */
	public boolean movePackages() throws PackageException {
		for (Iterator<Package> it = packagesInTransit.iterator(); it.hasNext();) {
			Package pkg = (Package) it.next();
			pkg.move();
		}
		if (packagesInTransit.size() > 0)
			return true;
		return false;
	}

	/**
	 * @return the name
	 */
	public String getName() {
		return name;
	}

	/**
	 * @param name
	 *            the name to set
	 * @throws FranchiseException
	 *             illegal name
	 */
	public void setName(String name) throws FranchiseException {
		Validator
				.validatePhrase(name, new FranchiseException("Nome inválido."));
		this.name = name;
		firePropertyChanged("name");
	}

	/**
	 * @return the address
	 */
	public Address getAddress() {
		return address;
	}

	/**
	 * @param address
	 *            the address to set
	 * @throws FranchiseException
	 *             illegal address
	 */
	public void setAddress(Address address) throws FranchiseException {
		Validator.validateObject(address, new FranchiseException(
				"Endereço inválido."));
		this.address = address;
		firePropertyChanged("address");
	}

	/**
	 * Does this Franchise has an airport?
	 * 
	 * @return true if it has an airport, false otherwise
	 */
	public boolean hasAirport() {
		return (airport == null);
	}

	/**
	 * Returns the nearest airport if this Franchise does not have one or null
	 * if it does have one.
	 */
	public Franchise getNearestAirport() {
		return airport;
	}

	/**
	 * Update airport existence status.
	 * 
	 * @param hasAirport
	 *            does this franchise has an airport?
	 */
	public void setHasAirport(boolean hasAirport) {
		if (hasAirport) {
			this.airport = null;
		} else {
			// TODO setHasAirport() - get nearest airport when airport is
			// removed
		}
		firePropertyChanged("hasAirport");
	}

	/**
	 * @return the packagesHandled
	 */
	public int getPackagesHandled() {
		return packagesHandled;
	}

	/**
	 * @return the sales
	 */
	public int getSales() {
		return sales;
	}

	/**
	 * Add a near franchise to the list.
	 * 
	 * @throws FranchiseException
	 *             null franchise or same franchise
	 * @return true if succeeded, false if the Franchise was already on the list
	 */
	public boolean addNearestFranchise(Franchise f) throws FranchiseException {
		Validator.validateObject(f, new FranchiseException(
				"null franchise received."));
		if (this.equals(f))
			throw new FranchiseException(
					"Não se pode conectar uma franquia a si mesma.");
		if (!nearestFranchises.contains(f)) {
			nearestFranchises.add(f);
			f.addNearestFranchise(this);
			firePropertyChanged("nearestFranchises");
			return true;
		}
		return false;
	}

	/**
	 * Remove a near franchise from the list.
	 * 
	 * @throws FranchiseException
	 *             null franchise
	 * @return true if it was removed, false if it was not on the list
	 */
	public boolean removeNearestFranchise(Franchise f)
			throws FranchiseException {
		Validator.validateObject(f, new FranchiseException(
				"null franchise received."));
		if (this.equals(f))
			throw new FranchiseException(
					"Não se pode remover uma conexão impossível: uma franquia conectada a ela mesma.");
		if (nearestFranchises.contains(f)) {
			nearestFranchises.remove(f);
			f.removeNearestFranchise(this);
			firePropertyChanged("nearestFranchises");
			return true;
		}
		return false;
	}

	/**
	 * @return the nearestFranchises
	 */
	public List<Franchise> getNearestFranchises() {
		return nearestFranchises;
	}

	/**
	 * Registers a new FranchiseEvent listener.
	 * 
	 * @param franchiseListener
	 *            the listener to register
	 */
	public synchronized void addFranchiseListener(FranchiseListener f) {
		if (!franchiseListeners.contains(f)) {
			franchiseListeners.add(f);
		}
	}

	/**
	 * Unregister a FranchiseEvent listener.
	 * 
	 * @param franchiseListener
	 *            the listener to unregister
	 */
	public synchronized void removeFranchiseListener(FranchiseListener f) {
		franchiseListeners.remove(f);
	}

	/**
	 * Fires property changed event.
	 */
	@SuppressWarnings("unchecked")
	public void firePropertyChanged(String property) {
		Collection<FranchiseListener> listeners;
		synchronized (this) {
			listeners = (Collection) (((ArrayList) franchiseListeners).clone());
		}
		FranchiseEvent event = new FranchiseEvent(this, property);
		for (FranchiseListener f : listeners) {
			f.propertyChanged(event);
		}
	}

	/**
	 * Describes the franchise
	 * 
	 * @return description of the franchise
	 */
	@Override
	public String toString() {
		return String.format("%s  -  %s/%s", name, address.getCity(), address
				.getState());
	}

	/**
	 * HashCode method
	 * 
	 * @return the hashcode
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((address == null) ? 0 : address.hashCode());
		result = prime * result + ((airport == null) ? 0 : airport.hashCode());
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		result = prime * result + packagesHandled;
		result = prime * result + sales;
		result = prime * result + vertex;
		return result;
	}

	/**
	 * Checks for equality. There is only one Franchise per city, therefore only
	 * the city is compared here.
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null || getClass() != obj.getClass())
			return false;

		return getAddress().getCity().equals(
				((Franchise) obj).getAddress().getCity());
	}

	/**
	 * Checks if this franchise's information matches the passed arguments.
	 * 
	 * @param name
	 *            partial franchise name to compare, if it is an empty string it
	 *            compares true.
	 * @param city
	 *            partial city name to compare, if it is an empty string it
	 *            compares true.
	 * @return true if this franchise matches the arguments given, false
	 *         otherwise
	 */
	public boolean matches(String name, String city) {
		return ((name.trim().equals("") || getName().contains(name)) && (city
				.trim().equals("") || address.getCity().contains(city)));
	}

	@Override
	/*
	 * Compares the name of the cities of the franchises.
	 * 
	 * @return {@inheritDoc}
	 */
	public int compareTo(Franchise franchise) {
		return address.getCity().compareTo(franchise.getAddress().getCity());
	}

}
